package com.ddw.meedeeuh.shared.movie.scrapers.local;

import java.util.logging.Level;
import java.util.logging.Logger;

import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;

import com.ddw.meedeeuh.shared.movie.EditableActor;
import com.ddw.meedeeuh.shared.movie.EditableAudio;
import com.ddw.meedeeuh.shared.movie.EditableMovie;
import com.ddw.meedeeuh.shared.movie.EditableStreamDetails;
import com.ddw.meedeeuh.shared.movie.EditableSubtitle;
import com.ddw.meedeeuh.shared.movie.EditableVideo;
import com.ddw.meedeeuh.shared.movie.MovieSettings;
import com.ddw.meedeeuh.shared.movie.MovieSettings.MovieNfoConstants;

/**
 * Represents a SAX handler for parsing an xbmc movie nfo
 * @author cmcnicholas
 *
 */
class MovieNfoHandler extends DefaultHandler {

	private final Logger logger;
	private final EditableMovie movie;
	private final MovieSettings settings;
	private EditableActor currentActor = null;
	private EditableStreamDetails currentStreamDetails = null;
	private EditableAudio currentAudio = null;
	private EditableVideo currentVideo = null;
	private EditableSubtitle currentSubtitle = null;
	private boolean inTitle = false;
	private boolean inOriginalTitle = false;
	private boolean inSortTitle = false;
	private boolean inRating = false;
	private boolean inYear = false;
	private boolean inTop250 = false;
	private boolean inVotes = false;
	private boolean inOutline = false;
	private boolean inPlot = false;
	private boolean inTagline = false;
	private boolean inRuntime = false;
	private boolean inThumb = false;
	private boolean inMpaa = false;
	private boolean inPlayCount = false;
	private boolean inWatched = false;
	private boolean inId = false;
	private boolean inTrailer = false;
	private boolean inGenre = false;
	private boolean inCredits = false;
	private boolean inDirector = false;
	private boolean inActor = false;
	private boolean inActorName = false;
	private boolean inActorRole = false;
	private boolean inSet = false;
	private boolean inFileInfo = false;
	private boolean inStreamDetails = false;
	private boolean inStreamDetailsVideo = false;
	private boolean inStreamDetailsVideoCodec = false;
	private boolean inStreamDetailsVideoAspect = false;
	private boolean inStreamDetailsVideoWidth = false;
	private boolean inStreamDetailsVideoHeight = false;
	private boolean inStreamDetailsAudio = false;
	private boolean inStreamDetailsAudioCodec = false;
	private boolean inStreamDetailsAudioLanguage = false;
	private boolean inStreamDetailsAudioChannels = false;
	private boolean inStreamDetailsSubtitle = false;
	private boolean inStreamDetailsSubtitleLanguage = false;

	/**
	 * Creates a new movie nfo handler instance
	 * @param movie the movie to parse values from the nfo into
	 * @param settings the movie settings to use when parsing
	 * @param logger the logger to provide logging messages
	 */
	public MovieNfoHandler(EditableMovie movie, MovieSettings settings, Logger logger) {
		this.movie = movie;
		this.logger = logger;
		this.settings = settings;
	}

	@Override
	public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
		if(setInState(qName, true)) {
			logger.finest("Nfo start element found: ".concat(qName));
		}
		super.startElement(uri, localName, qName, attributes);
	}

	@Override
	public void endElement(String uri, String localName, String qName) throws SAXException {
		if(setInState(qName, false)) {
			logger.finest("Nfo end element found: ".concat(qName));
		}
		super.endElement(uri, localName, qName);
	}

	@Override
	public void characters(char[] ch, int start, int length) throws SAXException {
		if(this.inActorRole) {
			this.currentActor.setRole(getCharactersString(ch, start, length, this.currentActor.getRole()));
		} else if(this.inActorName) {
			this.currentActor.setName(getCharactersString(ch, start, length, this.currentActor.getName()));
		} else if(this.inStreamDetailsAudioChannels) {
			this.currentAudio.setChannels(getCharactersInteger(ch, start, length));
		} else if(this.inStreamDetailsAudioCodec) {
			this.currentAudio.setCodec(getCharactersString(ch, start, length, this.currentAudio.getCodec()));
		} else if(this.inStreamDetailsAudioLanguage) {
			this.currentAudio.setLanguage(getCharactersString(ch, start, length, this.currentAudio.getLanguage()));
		} else if(this.inStreamDetailsVideoAspect) {
			this.currentVideo.setAspect(getCharactersDouble(ch, start, length));
		} else if(this.inStreamDetailsVideoCodec) {
			this.currentVideo.setCodec(getCharactersString(ch, start, length, this.currentVideo.getCodec()));
		} else if(this.inStreamDetailsVideoHeight) {
			this.currentVideo.setHeight(getCharactersInteger(ch, start, length));
		} else if(this.inStreamDetailsVideoWidth) {
			this.currentVideo.setWidth(getCharactersInteger(ch, start, length));
		} else if(this.inStreamDetailsSubtitleLanguage) {
			this.currentSubtitle.setLanguage(getCharactersString(ch, start, length, this.currentSubtitle.getLanguage()));
		} else if(this.inCredits) {
			this.movie.setCredits(getCharactersString(ch, start, length, this.movie.getCredits()));
		} else if(this.inDirector) {
			this.movie.setDirector(getCharactersString(ch, start, length, this.movie.getDirector()));
		} else if(this.inGenre) {
			this.movie.setGenre(getCharactersString(ch, start, length, this.movie.getGenre()));
		} else if(this.inId) {
			this.movie.setImdbId(getCharactersString(ch, start, length, this.movie.getImdbId()));
		} else if(this.inMpaa) {
			this.movie.setMpaa(getCharactersString(ch, start, length, this.movie.getMpaa()));
		} else if(this.inOriginalTitle) {
			this.movie.setOriginalTitle(getCharactersString(ch, start, length, this.movie.getOriginalTitle()));
		} else if(this.inOutline) {
			this.movie.setOutline(getCharactersString(ch, start, length, this.movie.getOutline()));
		} else if(this.inPlayCount) {
			this.movie.setPlayCount(getCharactersInteger(ch, start, length));
		} else if(this.inPlot) {
			this.movie.setPlot(getCharactersString(ch, start, length, this.movie.getPlot()));
		} else if(this.inRating) {
			this.movie.setRating(getCharactersDouble(ch, start, length));
		} else if(this.inRuntime) {
			this.movie.setRuntime(getCharactersInteger(ch, start, length));
		} else if(this.inSet) {
			this.movie.setSet(getCharactersString(ch, start, length, this.movie.getSet()));
		} else if(this.inSortTitle) {
			this.movie.setSortTitle(getCharactersString(ch, start, length, this.movie.getSortTitle()));
		} else if(this.inTagline) {
			this.movie.setTagLine(getCharactersString(ch, start, length, this.movie.getTagLine()));
		} else if(this.inThumb) {
			this.movie.setThumbnail(getCharactersString(ch, start, length, this.movie.getThumbnail()));
		} else if(this.inTitle) {
			this.movie.setName(getCharactersString(ch, start, length, this.movie.getName()));
		} else if(this.inTop250) {
			this.movie.setTop250(getCharactersBoolean(ch, start, length));
		} else if(this.inTrailer) {
			this.movie.setTrailer(getCharactersString(ch, start, length, this.movie.getTrailer()));
		} else if(this.inVotes) {
			this.movie.setVotes(getCharactersInteger(ch, start, length));
		} else if(this.inWatched) {
			this.movie.setWatched(getCharactersBoolean(ch, start, length));
		} else if(this.inYear) {
			this.movie.setYear(getCharactersInteger(ch, start, length));
		}
		super.characters(ch, start, length);
	}

	/**
	 * Retrieves the character string read from an nfo file,
	 * this method will append the string to an existing one as it is possible
	 * for the characters to be returned in chunks. This is especially the case
	 * with xml entities such as &amp; where it will return the string before
	 * the entity and after in seperate method calls
	 * @param ch the characters read
	 * @param start the start index of the read characters
	 * @param length the total length of the characters read
	 * @param appendTo the string to append to
	 * @return the character string or a concatenated character string
	 */
	private static String getCharactersString(char[] ch, int start, int length, String appendTo) {
		return appendTo != null ? appendTo.concat(new String(ch, start, length)) : new String(ch, start, length);
	}

	/**
	 * Retrieves the character string read from an nfo file and converts it
	 * to an integer value if possible
	 * @param ch the characters read
	 * @param start the start index of the read characters
	 * @param length the total length of the characters read
	 * @return an integer value or null
	 */
	private Integer getCharactersInteger(char[] ch, int start, int length) {
		try {
			return Integer.parseInt(getCharactersString(ch, start, length, null));
		} catch (NumberFormatException e) {
			logger.log(Level.FINEST, "Failed to parse as integer", e);
		}
		return null;
	}

	/**
	 * Retrieves the character string read from an nfo file and converts it
	 * to a double value if possible
	 * @param ch the characters read
	 * @param start the start index of the read characters
	 * @param length the total length of the characters read
	 * @return a double value or null
	 */
	private Double getCharactersDouble(char[] ch, int start, int length) {
		try {
			return Double.parseDouble(getCharactersString(ch, start, length, null));
		} catch (NumberFormatException e) {
			logger.log(Level.FINEST, "Failed to parse as double", e);
		}
		return null;
	}

	/**
	 * Retrieves the character string read from an nfo file and converts it
	 * to a boolean value if possible
	 * @param ch the characters read
	 * @param start the start index of the read characters
	 * @param length the total length of the characters read
	 * @return a boolean value or null
	 */
	private Boolean getCharactersBoolean(char[] ch, int start, int length) {
		final String string = getCharactersString(ch, start, length, null);

		// Check string length first
		if(string.length() == 0) {
			return null;
		}

		try {
			// Try to parse as integer secondly
			return Integer.parseInt(string) == 1;
		} catch (NumberFormatException e) {
			// If number parse fails, parse as true/false
			return Boolean.parseBoolean(string);
		}
	}

	/**
	 * Sets the in/out state of the element variables based
	 * on the element name the SAX parser has read
	 * @param qName the element name
	 * @param state the state on/off
	 * @return a flag indicating if an element was matched
	 */
	private boolean setInState(String qName, boolean state) {
		if(getNfoSettings().ACTOR.equalsIgnoreCase(qName)) {
			this.inActor = state;

			if(state) { // Create actor instance if element is opening
				this.currentActor = LocalActorFactory.create();
			} else { // Add actor to movie if element is closing
				this.movie.addActor(this.currentActor);
				this.currentActor = null;
			}
		} else if(this.inActor && getNfoSettings().ACTOR_NAME.equalsIgnoreCase(qName)) {
			this.inActorName = state;
		} else if(this.inActor && getNfoSettings().ACTOR_ROLE.equalsIgnoreCase(qName)) {
			this.inActorRole = state;
		} else if(getNfoSettings().FILE_INFO.equalsIgnoreCase(qName)) {
			this.inFileInfo = state;
		} else if(this.inFileInfo && getNfoSettings().STREAM_DETAILS.equalsIgnoreCase(qName)) {
			this.inStreamDetails = state;

			if(state) { // Create stream details instance if element is opening
				this.currentStreamDetails = LocalStreamDetailsFactory.create();
			} else { // Add stream details to movie if element is closing
				this.movie.addStreamDetail(this.currentStreamDetails);
				this.currentStreamDetails = null;
			}
		} else if(this.inFileInfo && inStreamDetails &&
				getNfoSettings().STREAM_DETAILS_VIDEO.equalsIgnoreCase(qName)) {
			this.inStreamDetailsVideo = state;

			if(state) { // Create video instance if element is opening
				this.currentVideo = LocalVideoFactory.create();
			} else { // Add video to stream details if element is closing
				this.currentStreamDetails.addVideo(this.currentVideo);
				this.currentVideo = null;
			}
		} else if(this.inFileInfo && inStreamDetails && inStreamDetailsVideo &&
				getNfoSettings().STREAM_DETAILS_VIDEO_CODEC.equalsIgnoreCase(qName)) {
			this.inStreamDetailsVideoCodec = state;
		} else if(this.inFileInfo && inStreamDetails && inStreamDetailsVideo &&
				getNfoSettings().STREAM_DETAILS_VIDEO_ASPECT.equalsIgnoreCase(qName)) {
			this.inStreamDetailsVideoAspect = state;
		} else if(this.inFileInfo && inStreamDetails && inStreamDetailsVideo &&
				getNfoSettings().STREAM_DETAILS_VIDEO_WIDTH.equalsIgnoreCase(qName)) {
			this.inStreamDetailsVideoWidth = state;
		} else if(this.inFileInfo && inStreamDetails && inStreamDetailsVideo &&
				getNfoSettings().STREAM_DETAILS_VIDEO_HEIGHT.equalsIgnoreCase(qName)) {
			this.inStreamDetailsVideoHeight = state;
		} else if(this.inFileInfo && inStreamDetails &&
				getNfoSettings().STREAM_DETAILS_AUDIO.equalsIgnoreCase(qName)) {
			this.inStreamDetailsAudio = state;

			if(state) { // Create audio instance if element is opening
				this.currentAudio = LocalAudioFactory.create();
			} else { // Add audio to stream details if element is closing
				this.currentStreamDetails.addAudio(this.currentAudio);
				this.currentAudio = null;
			}
		} else if(this.inFileInfo && inStreamDetails && inStreamDetailsAudio &&
				getNfoSettings().STREAM_DETAILS_AUDIO_CODEC.equalsIgnoreCase(qName)) {
			this.inStreamDetailsAudioCodec = state;
		} else if(this.inFileInfo && inStreamDetails && inStreamDetailsAudio &&
				getNfoSettings().STREAM_DETAILS_AUDIO_LANGUAGE.equalsIgnoreCase(qName)) {
			this.inStreamDetailsAudioLanguage = state;
		} else if(this.inFileInfo && inStreamDetails && inStreamDetailsAudio &&
				getNfoSettings().STREAM_DETAILS_AUDIO_CHANNELS.equalsIgnoreCase(qName)) {
			this.inStreamDetailsAudioChannels = state;
		} else if(this.inFileInfo && inStreamDetails &&
				getNfoSettings().STREAM_DETAILS_SUBTITLE.equalsIgnoreCase(qName)) {
			this.inStreamDetailsSubtitle = state;

			if(state) { // Create audio instance if element is opening
				this.currentSubtitle = LocalSubtitleFactory.create();
			} else { // Add audio to stream details if element is closing
				this.currentStreamDetails.addSubtitle(this.currentSubtitle);
				this.currentSubtitle = null;
			}
		} else if(this.inFileInfo && inStreamDetails && inStreamDetailsSubtitle &&
				getNfoSettings().STREAM_DETAILS_SUBTITLE_LANGUAGE.equalsIgnoreCase(qName)) {
			this.inStreamDetailsSubtitleLanguage = state;
		} else if(getNfoSettings().TITLE.equalsIgnoreCase(qName)) {
			this.inTitle = state;
		} else if(getNfoSettings().ORIGINAL_TITLE.equalsIgnoreCase(qName)) {
			this.inOriginalTitle = state;
		} else if(getNfoSettings().SORT_TITLE.equalsIgnoreCase(qName)) {
			this.inSortTitle = state;
		} else if(getNfoSettings().RATING.equalsIgnoreCase(qName)) {
			this.inRating = state;
		} else if(getNfoSettings().YEAR.equalsIgnoreCase(qName)) {
			this.inYear = state;
		} else if(getNfoSettings().TOP250.equalsIgnoreCase(qName)) {
			this.inTop250 = state;
		} else if(getNfoSettings().VOTES.equalsIgnoreCase(qName)) {
			this.inVotes = state;
		} else if(getNfoSettings().OUTLINE.equalsIgnoreCase(qName)) {
			this.inOutline = state;
		} else if(getNfoSettings().PLOT.equalsIgnoreCase(qName)) {
			this.inPlot = state;
		} else if(getNfoSettings().TAGLINE.equalsIgnoreCase(qName)) {
			this.inTagline = state;
		} else if(getNfoSettings().RUNTIME.equalsIgnoreCase(qName)) {
			this.inRuntime = state;
		} else if(getNfoSettings().THUMB.equalsIgnoreCase(qName)) {
			this.inThumb = state;
		} else if(getNfoSettings().MPAA.equalsIgnoreCase(qName)) {
			this.inMpaa = state;
		} else if(getNfoSettings().PLAY_COUNT.equalsIgnoreCase(qName)) {
			this.inPlayCount = state;
		} else if(getNfoSettings().WATCHED.equalsIgnoreCase(qName)) {
			this.inWatched = state;
		} else if(getNfoSettings().ID.equalsIgnoreCase(qName)) {
			this.inId = state;
		} else if(getNfoSettings().TRAILER.equalsIgnoreCase(qName)) {
			this.inTrailer = state;
		} else if(getNfoSettings().GENRE.equalsIgnoreCase(qName)) {
			this.inGenre = state;
		} else if(getNfoSettings().CREDITS.equalsIgnoreCase(qName)) {
			this.inCredits = state;
		} else if(getNfoSettings().DIRECTOR.equalsIgnoreCase(qName)) {
			this.inDirector = state;
		} else if(getNfoSettings().SET.equalsIgnoreCase(qName)) {
			this.inSet = state;
		} else {
			// Catch all cases that arent handled
			return false;
		}

		// Rather than return true for all if cases,
		// this will only execute if a case is matched
		return true;
	}

	/**
	 * Gets the movie nfo settings
	 * @return a movie nfo settings instance
	 */
	private MovieNfoConstants getNfoSettings() {
		return this.settings.getNfoConstants();
	}

}